/****************************************************************************
 * Copyright (c) 2006 Parity Communications, Inc.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *     Sergey Yakovlev - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.ecf.internal.provider.rss.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.net.MalformedURLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.TimeZone;

import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.internal.provider.rss.RssDebugOptions;
import org.eclipse.ecf.internal.provider.rss.RssPlugin;

/**
 * A HTTP message. It can be read from an InputStream or constructed from
 * scratch. The body of the message is only read on demand and cached in a
 * byte[] for later use. Provides functions for getting and setting headers
 * 
 */
public class HttpMessage {

	protected static String DEFAULT_VERSION = "HTTP/1.1";

	protected String startLine = null;
	protected Hashtable headers = new Hashtable(20);
	protected boolean hasBody = false;
	protected byte[] body = null;
	protected int length = 0;

	private InputStream in = null;

	/** Creates a new instance of HttpMessage */
	public HttpMessage() {
		super();
	}

	public HttpMessage(InputStream in) throws MalformedURLException, IOException {
		this.in = in;
		readHead();
	}

	protected void trace(String msg) {
		Trace.trace(RssPlugin.PLUGIN_ID, RssDebugOptions.DEBUG, msg);
	}

	protected void dumpStack(String msg, Throwable e) {
		Trace.catching(RssPlugin.PLUGIN_ID, RssDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), "", e);
	}

	/**
	 * Reads the head of the message (startline and headers) from the
	 * InputStream
	 * @throws IOException 
	 */
	public void readHead() throws IOException {
		trace("readHead start");
		String line;
		// read startline & ignore empty lines before startline
		for (int i = 0; i < 100; i++) {
			if (!((startLine = readLine(in)).equals(""))) {
				break;
			}
		}

		if (startLine.equals("")) {
			trace("startLine empty");
			throw new IOException();
		}
		// Logs startLine
		trace("startLine: " + startLine);
		// read headers until CRLF
		String actHeader = null;
		while (!(line = readLine(in)).equals("")) {
			if (line.startsWith(" ") || line.startsWith("\t")) {
				// multiline header
				if (actHeader != null) {
					headers.put(actHeader, ((String) headers.get(actHeader)) + "\n" + line.trim());
				}
			} else {
				// header
				actHeader = (line.substring(0, line.indexOf(":"))).toLowerCase();
				if (!headers.containsKey(actHeader)) {
					// normal case
					headers.put(actHeader, ((line.substring(line.indexOf(":") + 1)).trim()));
				} else {
					// header is already defined -> make comma seperated list
					// RFC2086 page 31
					String list = (String) headers.get(actHeader);
					list += "," + (line.substring(line.indexOf(":") + 1)).trim();
					headers.put(actHeader, list);
				}
			}
			// Logs header
			trace(line);
		}
	}

	public String getHeader(String name) {
		return (String) headers.get(name.toLowerCase());
	}

	public Date getDateHeader(String name) {
		Date date;
		SimpleDateFormat dateFormatter;
		final String header = this.getHeader(name);

		// try rfc 1123 date first
		dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
		dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));

		try {
			date = dateFormatter.parse(header);
			return date;
		} catch (final java.text.ParseException e1) {
			// ok, now try rfc 850 date
			dateFormatter = new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.ENGLISH);
			dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));

			try {
				date = dateFormatter.parse(header);
				return date;
			} catch (final java.text.ParseException e2) {
				// last, try asctime date format
				dateFormatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.ENGLISH);
				dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));

				try {
					date = dateFormatter.parse(header);
					return date;
				} catch (final java.text.ParseException e3) {
					trace("Could not parse date: " + e3.getMessage());
				}
			}
		}
		return null;
	}

	public Enumeration getHeaders() {
		return headers.keys();
	}

	public void setHeader(String name, String value) {
		headers.put(name.toLowerCase(), value);
	}

	public String setDateHeader(String name, Date value) {
		// make rfc 1123 (and rfc 2068) compliant date
		final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
		dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));

		final String strDate = dateFormatter.format(value).toString();
		setHeader(name, strDate);
		return strDate;
	}

	public void removeHeader(String name) {
		headers.remove(name.toLowerCase());
	}

	public boolean containsHeader(String name) {
		return headers.containsKey(name.toLowerCase());
	}

	public boolean hasBody() {
		return hasBody;
	}

	/**
	 * retrieves the body of the message as a byte[]. If the body is not read in
	 * yet, it is read from the InputStream and cached in an internal array
	 * @return byte []
	 * @throws IOException 
	 */
	public byte[] getBody() throws IOException {
		final ByteArrayOutputStream b = new ByteArrayOutputStream();
		readBody(b);
		body = b.toByteArray();
		return body;
	}

	/**
	 * sets the body of the message and therefore overrides a received body
	 * @param body 
	 */
	public void setBody(byte[] body) {
		this.body = body;
		length = body.length;
		hasBody = true;
	}

	/**
	 * overridden by HttpRequest and HttpResponse
	 */
	protected String getStartLine() {
		return startLine;
	}

	/**
	 * writes the message (startline, headers, body) out to the specified
	 * OutputStream
	 */
	protected void writeToStream(String startLine, OutputStream out) throws IOException {
		out.write((startLine + "\n").getBytes());

		final Enumeration e = headers.keys();
		if (body != null) {
			setHeader("content-length", String.valueOf(body.length));
		}
		// write headers out:
		// * we should not change the order, although it should not matter...
		while (e.hasMoreElements()) {
			final String hName = (String) e.nextElement();
			out.write((hName + ": " + (String) headers.get(hName) + "\n").getBytes());
		}

		out.write(("\n").getBytes());

		// write body out
		if (hasBody) {
			readBody(out);
		}
	}

	/**
	 * change default version
	 * @param version 
	 */
	public void setDefaultVersion(String version) {
		DEFAULT_VERSION = version;
	}

	/**
	 * utility function: reads a header line from the stream according to rfc
	 * (taken from muffin)
	 */
	protected String readLine(InputStream in) throws IOException {
		char buf[] = new char[128];
		int offset = 0;
		int ch;

		while ((ch = in.read()) > -1) {
			if (ch == '\n') {
				break;
			} else if (ch == '\r') {
				final int tmpch = in.read();
				if (tmpch != '\n') {
					if (!(in instanceof PushbackInputStream)) {
						trace("creating PushbackInputStream");
						in = new PushbackInputStream(in);
					}
					((PushbackInputStream) in).unread(tmpch);
				}
				break;
			} else {
				if (offset == buf.length) {
					final char tmpbuf[] = buf;
					buf = new char[tmpbuf.length * 2];
					System.arraycopy(tmpbuf, 0, buf, 0, offset);
				}
				buf[offset++] = (char) ch;
			}
		}
		return String.copyValueOf(buf, 0, offset);
	}

	/**
	 * read body of message in and write it to specified OutputStream.
	 * content-length is determined according to RFC 2068 page 31 1.) if a body
	 * must not be present (hasBody == false), ignore body content. 2.) if
	 * chunked transfer-ecoding, this defines the length 3.) content-length
	 * header 4.) multipart/byteranges -> ignore, we can't handle them (todo!!)
	 * 5.) read all content in until server closes connection
	 */
	private void readBody(OutputStream out) throws IOException {
		if (hasBody && body == null) {
			if ((containsHeader("Transfer-Encoding")) && (getHeader("Transfer-Encoding").equals("chunked"))) {
				// decoding-routine for chunked transfer-encoding
				// (according to rfc 2616 section 19.4.6.)
				int n;
				int chunk_size;
				String line;
				final byte[] buffer = new byte[8192];
				final ByteArrayOutputStream byte_out = new ByteArrayOutputStream(8192);

				length = 0;

				line = readLine(in);
				chunk_size = Integer.parseInt(line.substring(0, line.indexOf(" ") > -1 ? line.indexOf(" ") : line.length()), 16);

				while (chunk_size > 0) {
					for (int len = 0; len < chunk_size;) {
						n = in.read(buffer, 0, Math.min(chunk_size - len, buffer.length));
						if (n == -1) {
							break;
						}
						len += n;
						byte_out.write(buffer, 0, n);
					}

					line = readLine(in); // trailing crlf

					length += chunk_size;
					line = readLine(in);
					chunk_size = Integer.parseInt(line.substring(0, line.indexOf(" ") > -1 ? line.indexOf(" ") : line.length()), 16);
				}

				// headers come last...
				String actHeader = null;

				while (!(line = readLine(in)).equals("")) {
					if (line.startsWith(" ") || line.startsWith("\t")) {
						// multiline header
						headers.put(actHeader, ((String) headers.get(actHeader)) + "\n" + line);
					} else {
						actHeader = (line.substring(0, line.indexOf(":"))).toLowerCase();
						headers.put(actHeader, ((line.substring(line.indexOf(":") + 1)).trim()));
					}
				}
				setHeader("content-length", "" + length);
				// Proxies/gateways MUST remove any transfer coding prior to
				// forwarding a message via a MIME-compliant protocol (RFC 2068,
				// sec.19.4.6).
				removeHeader("transfer-encoding");
				out.write(byte_out.toByteArray());
			} else if (headers.containsKey("content-length")) {
				// header defines length of content
				length = Integer.parseInt(getHeader("Content-Length"));

				int n;
				int len = 0;
				final byte buffer[] = new byte[8192];

				while (len < length) {
					n = in.read(buffer, 0, Math.min(length - len, buffer.length));
					if (n == -1) {
						break;
					}
					len += n;
					out.write(buffer, 0, n);
					out.flush();
				}
			} else {
				// just read until server closes connection
				int n;
				final byte buffer[] = new byte[8192];

				while (true) {
					n = in.read(buffer, 0, buffer.length);
					if (n == -1) {
						break;
					}
					out.write(buffer, 0, n);
					out.flush();
				}
			}
		} else if (hasBody) { // body != null
			out.write(body);
		}
	}
}
